import $ from "jquery"
import _ from "lodash"

const parserGutter = 'CodeMirror-hintgutter'

const questionTypes = new Set() // 题型
const difficultyValues = new Set() // 题目难度值
const sectionConfigs = new Map() // 全局设置
const questionTypeConfig = new Map() // 题型信息
const blankAnswerStyle = new Set() // 填空题答案形式

!["单选", "多选", "不定项", "判断", "问答", "填空"].forEach(x => questionTypes.add(x))
!["易", "中", "难"].forEach(x => difficultyValues.add(x))
!["完全一致", "仅顺序不一致", "仅供参考"].forEach(x => blankAnswerStyle.add(x))
sectionConfigs.set("题型", {key: 'questionType', value: questionTypes})
  .set("知识点", {key: 'questionSkill'})
  .set("题目难度", {key: 'difficulty', value: difficultyValues, defaultValue: '中'})
questionTypeConfig
  .set("单选", {
    choice: true, // 是否需要
    options: [2, 26],
    answer: {
      test: /^[A-Z]$/,
      msg: '单选题正确选项个数为<span>一个</span>'
    }
  })
  .set("判断", {
    judge: true,
    answer: {
      test: /^(正确|错误|对|错|是|否|√|×)$/,
      msg: '判断题的答案：正确|错误，对|错，是|否，√|×',
      pair: new Map([
        ['正确', {a: 'A', opts: ['正确', '错误']}],
        ['错误', {a: 'B', opts: ['正确', '错误']}],
        ['对', {a: 'A', opts: ['对', '错']}],
        ['错', {a: 'B', opts: ['对', '错']}],
        ['是', {a: 'A', opts: ['是', '否']}],
        ['否', {a: 'B', opts: ['是', '否']}],
        ['√', {a: 'A', opts: ['√', '×']}],
        ['×', {a: 'B', opts: ['√', '×']}]
      ])
    },
  })
  .set("多选", {
    choice: true,
    options: [3, 26],
    multiple: true,
    answer: {
      test: /^[A-Z]{2,}$/,
      msg: '多选题正确选项<span>至少有两个</span>'
    }
  })
  .set("不定项", {
    choice: true,
    multiple: true,
    options: [2, 26],
    answer: {
      test: /^[A-Z]{1,}$/,
      msg: '不定项题正确选项<span>至少有一个</span>'
    }
  })
  .set("填空", {
    blank: true,
    choice: false,
    answer: true
  })
  .set("问答", {
    choice: false,
  })


function getDefaultData() {
  return {
    configs: {},
    questions: [],
  }
}

function clearGutter(editor, lineNumber) {
  editor.setGutterMarker(lineNumber, parserGutter)
}

function clearIgnore(editor, lineNumber) {
  editor.getDoc().removeLineClass(lineNumber, 'text', 'ignore')
  clearGutter(editor, lineNumber)
}

function clearWidgets(widgets) {
  if (!widgets) {
    return
  }
  if (_.isArray(widgets)) {
    // widgets.forEach(widget => widget.clear())
    for (let i = 0; i < widgets.length; i++) {
      widgets[i].clear()
      widgets[i] = null
    }
    widgets.length = 0
  } else {
    widgets.clear()
  }
}
//添加样例内容
function addSampleHint(doc) {
  let infos = []

  doc.addLineWidget(0, $('<div>').attr({'class': 'hint sample'}).html(`设置默认题型，支持<strong>6</strong>种题型：<strong>${[...questionTypes]}</strong>`)[0])
  doc.addLineWidget(1, $('<div>').attr({'class': 'hint sample'}).html(`设置默认题目知识点，可以不指定(删除本行即可)`)[0])
  doc.addLineWidget(2, $('<div>').attr({'class': 'hint sample'}).html(`设置默认题目题目难度，默认值为<strong>中</strong>，支持：<strong>${[...difficultyValues]}</strong>`)[0])

  infos.push(`题干前两对中括号<strong>[ ]</strong>分别指定当前题目的<strong> 题型 </strong>和<strong> 知识点 </strong>`)
  infos.push(`，如果设置了全局默认题型，也可以省略`)
  infos.push(`如果仅有一对中括号，则代表<strong> 题型 </strong>`)
  doc.addLineWidget(3, $('<div>').attr({'class': 'hint sample'}).html(infos.join(''))[0])
  infos.length = 0

  infos.push(`选项以<strong> 大写字母. </strong>开头`)
  infos.push(`<br/><strong>单选题</strong>选项的个数不能少于<strong>2</strong>个`)
  doc.addLineWidget(4, $('<div>').attr({'class': 'hint sample'}).html(infos.join(''))[0])
  infos.length = 0

  infos.push(`选择题的答案用选项的<strong>字母序号</strong>代表正确选项，必须提供`)
  infos.push(`<br/><strong>单选题</strong>答案的正确选项只能有<strong>一</strong>个`)
  doc.addLineWidget(8, $('<div>').attr({'class': 'hint sample'}).html(infos.join(''))[0])
  infos.length = 0

  infos.push(`<span>解析</span>考生/学员查答案时可显示该信息，达到学习的目的，可以不提供`)
  doc.addLineWidget(9, $('<div>').attr({'class': 'hint sample'}).html(infos.join(''))[0])
  infos.length = 0

  infos.push(`<strong>多选题</strong>选项的个数不能少于<strong>3</strong>个`)
  doc.addLineWidget(14, $('<div>').attr({'class': 'hint sample'}).html(infos.join(''))[0])
  infos.length = 0

  infos.push(`<strong>多选题</strong>答案的正确选项至少有<strong>2</strong>个`)
  doc.addLineWidget(15, $('<div>').attr({'class': 'hint sample'}).html(infos.join(''))[0])
  infos.length = 0

  infos.push(`<strong>不定项选择题</strong>选项的个数不能少于<strong>2</strong>个`)
  doc.addLineWidget(20, $('<div>').attr({'class': 'hint sample'}).html(infos.join(''))[0])
  infos.length = 0

  infos.push(`<strong>不定项选择题</strong>答案的正确选项至少有<strong>1</strong>个`)
  doc.addLineWidget(21, $('<div>').attr({'class': 'hint sample'}).html(infos.join(''))[0])
  infos.length = 0

  infos.push(`<strong>判断题</strong>答案的取值：<strong>正确|错误，对|错，是|否，√|×</strong>`)
  doc.addLineWidget(23, $('<div>').attr({'class': 'hint sample'}).html(infos.join(''))[0])
  infos.length = 0

  infos.push(`<strong>填空题</strong>需要提供<span>填空数</span>、<span>答案形式</span>和<span>参考答案</span>`)
  doc.addLineWidget(24, $('<div>').attr({'class': 'hint sample'}).html(infos.join(''))[0])
  infos.length = 0

  infos.push(`<span>填空数</span>说明该题有多少个填空`)
  doc.addLineWidget(25, $('<div>').attr({'class': 'hint sample'}).html(infos.join(''))[0])
  infos.length = 0

  infos.push(`<span>答案形式</span>支持：<strong>${[...blankAnswerStyle]}</strong>`)
  infos.push(`<blockquote><strong>完全一致</strong>可自动判卷，答题者的答案必须与题目提供的答案完全一致，包括顺序`)
  infos.push(`<br/><strong>仅顺序不一致</strong>可自动判卷，答题者的答案需在提供的答案列表中，但顺序无关紧要`)
  infos.push(`<br/><strong>仅供参考</strong>需手工判卷，提供的答案仅作为手工判卷时的参考答案</blockquote>`)
  infos.push(`如果答案形式为仅供参考，那么可以不提供参考答案，否则必须提供`)
  doc.addLineWidget(26, $('<div>').attr({'class': 'hint sample'}).html(infos.join(''))[0])
  infos.length = 0

  infos.push(`<strong>参考答案</strong>根据<strong>填空数</strong>提供对应的答案`)
  infos.push(`<br/>如果某个空有多个答案，那么用三个竖线<strong>|||</strong>分开，代表<strong>或</strong>，考生答案匹配任意一个都算正确`)
  doc.addLineWidget(29, $('<div>').attr({'class': 'hint sample'}).html(infos.join(''))[0])
  infos.length = 0

  infos.push(`<strong>问答题</strong>目前需手工判卷，提供的答案仅作为手工判卷时的参考答案`)
  infos.push(`<br/>如果某个空有多个答案，那么用三个竖线<strong>|||</strong>分开，代表<strong>或</strong>`)
  doc.addLineWidget(31, $('<div>').attr({'class': 'hint sample'}).html(infos.join(''))[0])
  infos.length = 0

}
/*
*  编辑器内增加行提示
*  params:
*         doc:编辑器内容
*         lineNumber:在第几行增加提示
*         hint:提示文本内容
*         action:提示类型
*/
function addLineHint(doc, lineNumber, hint, style, action) {
  let inner = hint
  let $btn = null
  if (action) {
    $btn = $('<a>').attr({
      'class': 'btn-hint btn-' + action,
      'data-line': lineNumber,
      'data-action': action
    }).text(action == 'delete' ? '删除当前行' : action)
  }
  let $div = $('<div>').attr({
    'class': 'hint ' + (style || 'danger')
  }).append(inner)//.append($btn)
  return doc.addLineWidget(lineNumber, $div[0])
}
/*
*  编辑器内题目间增加分割线
*  params:
*         doc:编辑器内容
*         lineNumber:在第几行增加
*         
*/
function addSeperator(doc, lineNumber) {
  let $seperator = $('<hr/>')
  return doc.addLineWidget(lineNumber, $seperator[0], {
    above: true
  })
}
/*
*  当文本不能识别时，增加文本忽略的提示 
*  params:
*         editor:编辑器实例
*         lineNumber:在第几行增加
*         
*/
function addIgnore(editor, lineNumber) {
  editor.getDoc().addLineClass(lineNumber, 'text', 'ignore')
  addQuestionHint(editor, lineNumber, null, 'ignore')
}

const hintIcon = {
  error: 'fa fa-exclamation-circle',
  success: 'fa fa-check-circle',
  ignore: 'fa fa-exclamation-triangle'
}
/*
*  当文本不能识别时，增加文本忽略的提示 
*  params:
*         editor:编辑器实例
*         lineNumber:在第几行增加
*         hints：提示内容
*         style:提示样式
*         
*/
function addQuestionHint(editor, lineNumber, hints, style) {
    let template
    if (style === 'success') {
      template = `<i class="${hintIcon[style]}"></i><div class="q-tooltip  ${style}">该题信息完整</div>`
    } else if (style === 'error') {
      template = `<i class="${hintIcon[style]}"></i><div class="q-tooltip ${style}"><h4>该题有如下错误：</h4>
  <ol>${hints.map((hint, index) => `<li data-index="${index + 1}">${hint}</li>`).join('')}</ol></div>`
    } else if (style === 'ignore') {
      template = `<i class="${hintIcon[style]}"></i><div class="q-tooltip  ${style}">未能解析当前行，做<strong>忽略</strong>处理</div>`
    }

    let $hint = $('<div>').attr({
      'class': 'q-hint-trigger ' + (style || 'danger')
    }).html(template)
    editor.setGutterMarker(lineNumber, parserGutter, $hint[0])
}


const typeHint = `<blockquote><p><span>题型范围</span>${[...questionTypes]}</p></blockquote>`
const optionHint = `<blockquote><p><span>选项格式</span>英文大写字母.选项内容</p><p><span>举例</span>A.我是一个选项</p></blockquote>`
const optAnswerHint = `<blockquote><p><span>答案格式</span>答案：选项字母</p><p><span>举例</span>答案：AB</p></blockquote>`
const choiceAnswerHint = `<blockquote><p><span>答案格式</span>答案：正确|错误，对|错，是|否，√|×</p><p><span>举例</span>答案：正确</p></blockquote>`
const blanksHint = `<blockquote><p><span>填空数格式</span>填空数：n (n在1到20之间)</p><p><span>举例</span>填空数：3</p></blockquote>`
const blankAnswerHint = [`<blockquote><p><span>参考答案格式</span>参考答案n：答案内容 (n对应每个填空，从1开始)</p>`,
  `<p><span>举例1</span>假设填空数为<strong>3</strong>，那么参考答案为</p>`,
  `<blockquote><p><span>参考答案<strong>1</strong></span>答案1</p><p><span>参考答案<strong>2</strong></span>答案2</p><p><span>参考答案<strong>3</strong></span>答案3</p></blockquote>`,
  `<p><span>举例2</span>如果答案有多种情况，那么使用 <strong>|||</strong> 分隔</p>`,
  `<blockquote><p><span>参考答案<strong>1</strong></span>百度<strong>|||</strong>baidu</p><p>...</p></blockquote>`,
  , `</blockquote>`].join('')
/*
*  选项个数范围提示
*  params:
*         questionType:题目类型
*         min:最少选项个数
*         max:最多选项个数
*         
*/
function optCountHintFn(questionType, min, max) {
  let optCountHint = `<p>${questionType}的选项个数范围是<span>${min}</span>到<span>${max}</span>个</p>`
  return optCountHint
}
/*
* 检查题目数据
* params:
*       qData:题目属性的对象
*/
function checkQuestionData(qData) {
  let hints = []
  let typeConfig = questionTypeConfig.get(qData.questionType)

  if (!typeConfig) {
    hints.push(`缺少有效的题型` + typeHint)
    return hints
  }

  switch (qData.questionType) {
    case '单选':
    case '多选':
    case '不定项':
      let options = qData.options
      let optionCount = options && options.length
      let optAnswer = qData.refAnswer.optAnswer
      let [minOpt, maxOpt] = typeConfig.options
      // 选项 check
      if (optionCount) {
        if (!(optionCount && optionCount <= maxOpt && optionCount >= minOpt)) {
          hints.push(`选择题的选项个数错误` + optCountHintFn(qData.questionType, minOpt, maxOpt))
        } else if (_.uniq(options).length !== optionCount) {
          hints.push(`选择题存在<strong>重复的选项</strong>`)
        }
      } else {
        hints.push(`缺少选项` + optionHint)
      }

      if (hints.length) {
        break
      }
      // 答案 check
      if (!optAnswer) {
        hints.push(`缺少答案或者答案不合理` + optAnswerHint)
      }
      break
    case '判断':
      let choiceAnswer = qData.refAnswer.optAnswer
      if (!choiceAnswer) {
        hints.push(`缺少答案或者答案不合理` + choiceAnswerHint)
      }
    case '问答':
      break
    case '填空':
      let blanks = qData.blanks
      let answerStyle = qData.answerStyle
      let blankAnswer = qData.refAnswer.blankAnswer
      let title = qData.title;
      let blankCount = title.match(/_+/g);
      if (!blanks || !blankCount || blankCount.length != blanks) {
        hints.push(`缺少填空数、填空数不合理或者填空下划线不合理` + blanksHint)
      }

      // 当答案形式为非仅供参考时, 必须给出参考答案
      if (answerStyle !== '仅供参考') {
        if (!blankAnswer || !blankAnswer.length) {
          hints.push(`缺少参考答案` + blankAnswerHint)
        } else if (blankAnswer.length !== blanks) {
          hints.push(`参考答案数[<strong>${blankAnswer.length}</strong>]与填空数[<strong>${blanks}</strong>]不匹配，请补充完整` + blankAnswerHint)
        }
      }
  }

  return hints
}

function match(regExp) {
  return function (lineInfo) {
    regExp.lastIndex = 0 // reset
    return regExp.test(lineInfo.text)
  }
}


let matchComment = match(/^\/\//)
let matchBlank = match(/^\s*$/)
//匹配全局设置
let matchSectionConfig = match(/^(知识点|题型|题目难度)[:：].*/)
//匹配题目开始
let matchQuestionBegin = match(/^\d+[\.、][^\s]+/)
//匹配题目选项
let matchQuestionOption = match(/^[A-Z][\.\、\s]/)
//匹配题目类型
let matchQuestionProperty = function (questionType) {
  switch (questionType) {
    case '单选':
    case '多选':
    case '判断':
    case '不定项':
      return match(/^(答案|题目难度|解析)[:：]/)
    case '问答':
      return match(/^(题目难度|参考答案|解析)[:：]/)
    case '填空':
      return match(/^(填空数|参考答案.*|答案形式|题目难度|解析)[:：]/)
  }
}

// 状态接口
class State {
  constructor(context) {
    this.context = context
  }

  handle(lineText) {
    return
  }

  toString() {
    return this.constructor.name
  }

  //
  dispatch(lineText) {

  }
}
// 全局设置
class SectionConfigState extends State {
  constructor(context) {
    super(context)
  }

  // 题型：单选
  handle(lineInfo) {

    this.context.state = null

    let matches = /^(.*?)[:：\s](.*)$/g.exec(lineInfo.text)
    let doc = this.context.doc
    let configKey = matches[1].trim()
    let configValue = matches[2].trim()
    let configInfo = sectionConfigs.get(configKey)
    let prevData = lineInfo.data
    let returnData = {
      data: prevData,
      state: null,
    }

    if (configInfo) {
      // 如果已经存在该值，那么覆盖
      if (prevData.configs[configInfo.key]) {
        addLineHint(doc, lineInfo.line, configKey + '重复定义了')
        return returnData
      }
      if (!configValue) {
        addLineHint(doc, lineInfo.line, '请设置<span>' + configKey + '</span>的默认取值')
        return returnData
      }
      if (configInfo.value) {
        if (configInfo.value.has(configValue)) {
          prevData.configs[configInfo.key] = configValue
        } else {
          delete prevData.configs[configInfo.key]
          addLineHint(doc, lineInfo.line, configKey + '支持：' + [...configInfo.value])
          if (configInfo.defaultValue) {
            addLineHint(doc, lineInfo.line, configKey + '采用默认值：' + configInfo.defaultValue, 'info')
          }
        }
      } else {
        prevData.configs[configInfo.key] = configValue
      }
    } else {
      addLineHint(doc, lineInfo.line, '不识别的属性')
    }


    return returnData
  }

}
// 题目开始
class QuestionBeginState extends State {
  constructor(context) {
    super(context)
  }

  //题目标题匹配 1.[单选][公司文化]以下图片是哪个公司的logo？
  handle(lineInfo) {

    if (lineInfo.dispatch) {
      lineInfo.dispatch = false
      return this.dispatch(lineInfo)
    }
    this.context.state = null
    let matches = /^(\d+)[\.、](?:[\[|【](.*?)[\]|】])?(?:[\[|【](.*?)[\]|】])?(.*)$/g.exec(lineInfo.text)
    let questionIndex = matches[1] * 1
    let questionType = matches[2]
    let questionSkill = matches[3]
    let title = matches[4]

    let prevData = lineInfo.data
    let doc = this.context.doc
    let returnData = {
      state: null,
      data: prevData,
      dispatch: false,
      newQuestion: true
    }
    let qData = {
      ui: { // ui: 视图相关信息
        line: lineInfo.line
      },
      refAnswer: {}
    }

    let shouldQuestionIndex = prevData.questions.length + 1
    if (questionIndex != shouldQuestionIndex) {
      let cursor = doc.getCursor()
      doc.setSelection({
        line: lineInfo.line,
        ch: 0
      }, {
        line: lineInfo.line,
        ch: (questionIndex + '').length
      })
      doc.replaceSelection(shouldQuestionIndex + '')
      doc.setCursor(cursor)
    }

    prevData.questions.push(qData)

    addSeperator(doc, lineInfo.line)
    // 设置了 题型 和 知识点
    if (/^\d+[\.、]\[?【?.*\]\s*\[.*\]?】?/g.test(lineInfo.text)) {
      if (!questionTypes.has(questionType)) {
        addLineHint(doc, lineInfo.line, '请设置有效的题型：' + [...questionTypes])
        return returnData
      }
      let defaultSkill = prevData.configs.questionSkill
      if (!questionSkill && !defaultSkill) {
        addLineHint(doc, lineInfo.line, '请设置题目知识点', 'warning')
      }
    } else if (/^\d+[\.、]\[?】?.*\]?】?/g.test(lineInfo.text) && !questionTypes.has(questionType)) {
      addLineHint(doc, lineInfo.line, '请设置有效的题型：' + [...questionTypes])
      return returnData
    }

    questionType = questionType || prevData.configs.questionType
    questionSkill = questionSkill || prevData.configs.questionSkill

    if (!questionType) {
      addLineHint(doc, lineInfo.line, '请设置有效的题型：' + [...questionTypes])
      return returnData
    }

    returnData.state = this
    returnData.dispatch = true

    qData.questionType = questionType
    qData.questionSkill = questionSkill
    qData.difficulty = prevData.configs.difficulty || '中'
    qData.title = title
    if (questionType === '填空') {
      qData.answerStyle = '完全一致'
    }

    return returnData
  }

  dispatch(lineInfo) {
    // 如果是选择题，那么要匹配选项
    // 否则要匹配题目属性
    // 否则要匹配一道新的题目
    // 否则作为题干
    let prevData = lineInfo.data
    let qData = _.last(prevData.questions)
    let questionType = qData.questionType
    let typeInfo = questionTypeConfig.get(questionType)
    if (typeInfo.choice && matchQuestionOption(lineInfo)) {
      this.context.state = this.context.optionBeginState
    } else if (matchQuestionProperty(questionType)(lineInfo)) {
      this.context.state = this.context.questionPropertyBeginState
    } else if (matchQuestionBegin(lineInfo)) {
      this.context.state = this.context.questionBeginState
    } else {
      this.context.state = this.context.questionTitleState
    }

    return this.context.state.handle(lineInfo)
  }
}
// 纯题干
class QuestionTitleState extends State {
  constructor(context) {
    super(context)
  }

  handle(lineInfo) {
    if (lineInfo.dispatch) {
      lineInfo.dispatch = false
      return this.context.questionBeginState.dispatch.call(this, lineInfo)
    }

    let doc = this.context.doc
    let prevData = lineInfo.data
    let qData = _.last(prevData.questions)
    let returnData = {
      state: this,
      data: prevData,
      dispatch: true
    }
    qData.title += ('\n' + lineInfo.text)
    return returnData
  }

}
//选项A开始
class OptionBeginState extends State {
  constructor(context) {
    super(context)
  }

  handle(lineInfo) {
    if (lineInfo.dispatch) {
      lineInfo.dispatch = false
      return this.dispatch(lineInfo)
    }
    let doc = this.context.doc

    let matches = /^([A-Z])[\.\、\s](.*)$/g.exec(lineInfo.text)
    let optionIndex = matches[1]
    let optionContent = matches[2]

    let prevData = lineInfo.data
    let qData = _.last(prevData.questions)
    let options = qData.options
    if (!qData.options) {
      options = qData.options = []
      qData.refAnswer.optAnswer = ''
    }
    let returnData = {
      state: this,
      data: prevData,
      dispatch: true
    }

    let shouldOptionIndex = String.fromCharCode('A'.charCodeAt() + options.length)
    if (optionIndex != shouldOptionIndex) {
      let cursor = doc.getCursor()
      doc.setSelection({
        line: lineInfo.line,
        ch: 0
      }, {
        line: lineInfo.line,
        ch: (optionIndex + '').length
      })
      doc.replaceSelection(shouldOptionIndex + '')
      doc.setCursor(cursor)
    }

    options.push(optionContent)
    return returnData
  }

  dispatch(lineInfo) {
    let prevData = lineInfo.data
    let qData = _.last(prevData.questions)
    let questionType = qData.questionType
    // let typeInfo = questionTypeConfig.get(questionType)
    if (matchQuestionOption(lineInfo)) {
      this.context.state = this.context.optionBeginState
    } else if (matchQuestionProperty(questionType)(lineInfo)) {
      this.context.state = this.context.questionPropertyBeginState
    } else if (matchQuestionBegin(lineInfo)) {
      this.context.state = this.context.questionBeginState
    } else {
      this.context.state = this.context.optionContentState
    }

    return this.context.state.handle(lineInfo)
  }
}
// 纯选项内容
class OptionContentState extends State {
  constructor(context) {
    super(context)
  }

  handle(lineInfo) {
    if (lineInfo.dispatch) {
      lineInfo.dispatch = false
      return this.context.optionBeginState.dispatch.call(this, lineInfo)
    }

    let doc = this.context.doc
    let prevData = lineInfo.data
    let qData = _.last(prevData.questions)
    let options = qData.options
    let returnData = {
      state: this,
      data: prevData,
      dispatch: true
    }
    options[options.length - 1] += ('\n' + lineInfo.text)
    return returnData
  }

}
// 题目属性
class QuestionPropertyBeginState extends State {
  constructor(context) {
    super(context)
  }

  handle(lineInfo) {
    if (lineInfo.dispatch) {
      lineInfo.dispatch = false
      return this.dispatch(lineInfo)
    }
    let doc = this.context.doc
    let matches = /^(.*)[:：\s](.*)$/g.exec(lineInfo.text)
    let propertyKey = matches[1]
    let propertyValue = matches[2].trim()

    let prevData = lineInfo.data
    let qData = _.last(prevData.questions)
    let questionType = qData.questionType
    let typeInfo = questionTypeConfig.get(questionType)

    let returnData = {
      state: this,
      data: prevData,
      dispatch: true
    }
    if (propertyKey === '题目难度') {
      if (difficultyValues.has(propertyValue)) {
        qData.difficulty = propertyValue
      } else {
        addLineHint(doc, lineInfo.line, propertyKey + '的范围：' + [...difficultyValues], 'warning')
        addLineHint(doc, lineInfo.line, '题目难度采用默认设置：' + (prevData.configs.difficulty || '中'), 'info')
      }

    } else if (propertyKey === '解析') {
      returnData.propertyKey = 'answerExplain'
      qData.answerExplain = propertyValue

    } else if (propertyKey === '参考答案' && !typeInfo.blank) {
      returnData.propertyKey = 'textAnswer'
      qData.refAnswer.textAnswer = propertyValue

    } else if (propertyKey === '答案') {

      if (typeInfo.choice) {
        let options = qData.options
        let optionCount = options && options.length || 26
        let ansInfo = typeInfo.answer
        let formatAnswer = _.remove(_.uniq(propertyValue.toUpperCase().split('').sort()), function (x) {

          return new RegExp('[A-' + String.fromCharCode('A'.charCodeAt() + optionCount - 1) + ']', 'i').test(x)
        }).join('')

        ansInfo.test.lastIndex = 0
        // 对比答案
        if (formatAnswer && ansInfo.test.test(formatAnswer)) {
          if (formatAnswer.toUpperCase() !== propertyValue.toUpperCase()) {
            addLineHint(doc, lineInfo.line, '答案填写错误：' + propertyValue, 'error')
            addLineHint(doc, lineInfo.line, '答案已帮您解析为：' + formatAnswer, 'info')
            propertyValue = formatAnswer
          }

          qData.refAnswer.optAnswer = propertyValue.toUpperCase();
        } else {
          addLineHint(doc, lineInfo.line, '答案解析错误：' + ansInfo.msg)
        }

      } else if (typeInfo.judge) {
        let ansInfo = typeInfo.answer
        ansInfo.test.lastIndex = 0
        if (!ansInfo.test.test(propertyValue)) {
          addLineHint(doc, lineInfo.line, ansInfo.msg)
        } else {
          let ansData = ansInfo.pair.get(propertyValue)
          qData.refAnswer.optAnswer = ansData.a
          qData.options = ansData.opts
        }
      } else {
        addLineHint(doc, lineInfo.line, '非客观题请使用[参考答案]')
      }

    } else if (propertyKey === '填空数') {
      if (!/^\d+$/.test(propertyValue)) {
        addLineHint(doc, lineInfo.line, '请填写有效的数字')
      } else if (propertyValue * 1 > 20 || propertyValue * 1 < 1) {
        addLineHint(doc, lineInfo.line, '填空数范围：[1 - 20]')
      } else {
        qData.blanks = propertyValue * 1
      }

    } else if (propertyKey === '答案形式') {
      if (!blankAnswerStyle.has(propertyValue)) {
        addLineHint(doc, lineInfo.line, '答案形式可填：' + [...blankAnswerStyle] + '。默认为[完全一致]', 'warning')
        addLineHint(doc, lineInfo.line, '答案形式采用默认值：完全一致', 'info')
      } else {
        qData.answerStyle = propertyValue
      }

    } else if (/^参考答案(.*)/.exec(propertyKey)) {
      let answerIndex = RegExp.$1.trim()

      if (!qData.blanks) {
        addLineHint(doc, lineInfo.line, '请先设置[填空数]')
        return returnData
      }

      if (!qData.refAnswer.blankAnswer) {
        qData.refAnswer.blankAnswer = []
      }

      let shouldAnswerIndex = qData.refAnswer.blankAnswer.length + 1

      if (answerIndex.trim() != shouldAnswerIndex) {
        let cursor = doc.getCursor()
        doc.setSelection({
          line: lineInfo.line,
          ch: 4
        }, {
          line: lineInfo.line,
          ch: 4 + (answerIndex + '').length
        })
        doc.replaceSelection(shouldAnswerIndex + '')
        doc.setCursor(cursor)
      }

      if (shouldAnswerIndex <= qData.blanks) {
        qData.refAnswer.blankAnswer.push(propertyValue)
      } else {
        addLineHint(doc, lineInfo.line, '参考答案的个数大于设置的填空数[' + qData.blanks + ']，忽略本行参考答案', 'warning')
      }
    }

    // console.log(doc.getEditor().lineInfo(lineInfo.line))
    return returnData
  }

  dispatch(lineInfo) {
    let prevData = lineInfo.data
    let qData = _.last(prevData.questions)
    let questionType = qData.questionType
    let typeInfo = questionTypeConfig.get(questionType)
    if (matchQuestionProperty(questionType)(lineInfo)) {
      this.context.state = this.context.questionPropertyBeginState
    } else if (matchQuestionBegin(lineInfo)) {
      this.context.state = this.context.questionBeginState
    } else {
      this.context.state = this.context.questionPropertyContentState
    }

    return this.context.state.handle(lineInfo)
  }

}
// 纯题目属性值
class QuestionPropertyContentState extends State {
  constructor(context) {
    super(context)
  }

  handle(lineInfo) {
    if (lineInfo.dispatch) {
      lineInfo.dispatch = false
      return this.context.questionPropertyBeginState.dispatch.call(this, lineInfo)
    }

    let doc = this.context.doc
    let editor = this.context.editor
    let prevData = lineInfo.data
    let qData = _.last(prevData.questions)
    let propertyKey = lineInfo.propertyKey
    let returnData = {
      state: this,
      data: prevData,
      dispatch: true,
      propertyKey: propertyKey
    }

    if (!propertyKey) {
      if (!matchBlank(lineInfo)) {
        addIgnore(editor, lineInfo.line)
      }
    } else if (/Answer$/.test(propertyKey)) {
      qData.refAnswer[propertyKey] += ('\n' + lineInfo.text)
    } else {
      qData[propertyKey] += ('\n' + lineInfo.text)
    }
    return returnData
  }

}

class Context {
  sectionConfigState // 全局设置
  questionBeginState // 题目开始
  questionTitleState // 纯题干
  optionBeginState // 选项开始
  optionContentState // 纯选项内容
  questionPropertyBeginState // 题目属性
  questionPropertyContentState // 纯题目属性值

  state // current state

  constructor() {
    this.sectionConfigState = new SectionConfigState(this)
    this.questionBeginState = new QuestionBeginState(this)
    this.questionTitleState = new QuestionTitleState(this)
    this.optionBeginState = new OptionBeginState(this)
    this.optionContentState = new OptionContentState(this)
    this.questionPropertyBeginState = new QuestionPropertyBeginState(this)
    this.questionPropertyContentState = new QuestionPropertyContentState(this)
  }

  request(lineInfo) {

    let returnData = {
      data: lineInfo.data,
      state: lineInfo.state,
      dispatch: lineInfo.dispatch
    }
    let hasQuestion = lineInfo.data.questions && lineInfo.data.questions.length

    this.state = lineInfo.state
    if (this.state) {
      return this.state.handle(lineInfo)
    }

    if (!hasQuestion && matchSectionConfig(lineInfo)) {
      this.state = this.sectionConfigState
    } else if (matchQuestionBegin(lineInfo)) {
      this.state = this.questionBeginState
    } else {
      if (!matchBlank(lineInfo)) {
        addIgnore(this.editor, lineInfo.line)
      }
      return returnData
    }

    return this.state.handle(lineInfo)
  }
}

class Parser extends Context {

  editor // codemirror editor
  doc // codemirror doc
  value // editor value
  data // converted question object from value
  // index - lineNumber
  // value -
  lineCache = []

  parseHandler
  delay = 500

  onChange() {
  }

  setSample(val) {
    this.sample = val
  }

  constructor(codeMirrorEditor, options) {
    super()
    this.editor = codeMirrorEditor
    this.doc = this.editor.getDoc()

    this.delay = options.delay || this.delay
    this.onChange = options.onChange || onChange

    this.editor.setOption('gutters', ['CodeMirror-linenumbers', parserGutter])

    this.bindEvent()
  }

  bindEvent() {
    let self = this
    let start
    let lock = false
    // {from, to, text, removed, origin}
    this.editor.on('change', function (codemirror, changeObj) {
      if (!lock) {
        lock = true
        start = changeObj && changeObj.from.line || 0
      }
      clearTimeout(self.parseHandler)
      self.parseHandler = setTimeout(function () {
        lock = false
        self.editor.operation(function () {
          self.parse(start)
        });
        self.onChange()
      }, self.delay)
    })
    $(this.editor.getWrapperElement()).on('click', '.hint .btn-hint', function (e) {
      let $btn = $(e.target).closest('.btn-hint')
      let action = $btn.data('action')
      let line = $btn.data('line')

      if (action === 'delete') {
        self.doc.setCursor({line: line})
        self.editor.execCommand('deleteLine')
      }
    })
  }

  parse(changeStart) {
    let self = this
    let start = 0 || changeStart
    let end = this.doc.lineCount()

    this.lineCache.length = start
    // end: not including
    this.doc.eachLine(start, end, function (lineHandle) {
      let lineNumber = this.doc.getLineNumber(lineHandle)
      let prev
      if (lineNumber > 0) {
        // note: cloneDeep here is important, or lineCache values shared common inner Object
        // let beCloned = this.lineCache[lineNumber - 1]
        prev = _.cloneDeepWith(this.lineCache[lineNumber - 1], function (value) {
          if (value instanceof State) {
            return value
          }
        })
      } else {
        prev = {
          data: getDefaultData()
        }
      }

      // 返回
      // 1. data - 以 当前行 为止解析的数据对象
      // 2. state - 解析 当前行 对应的 状态
      // 3. 更多作为 state 内部控制的参数...
      //    disptach
      //    isQuestionBegin
      let result = this.parseLine(lineHandle, prev)

      this.lineCache[lineNumber] = result
    }.bind(this))

    this.data = this.lineCache[end - 1].data

    let questionCount = this.data.questions.length

    this.data.questions.forEach((qData, index) => {
      let hints = checkQuestionData(qData)
      if (hints.length) {
        addQuestionHint(self.editor, qData.ui.line, hints, 'error')
        qData._hasError = true
      } else {
        addQuestionHint(self.editor, qData.ui.line, hints, 'success')
        qData._hasError = false
      }
    })

    if (this.sample) {
      addSampleHint(this.doc)
    }

  }

  parseLine(lineHandle, prev) {
    let lineInfo = this.editor.lineInfo(lineHandle)
    clearWidgets(lineInfo.widgets)
    clearIgnore(this.editor, lineInfo.line)
    clearGutter(this.editor, lineInfo.line)
    return this.request(_.extend({}, prev, lineInfo))
  }

}

export default Parser
